package solverLogic;

import gui.controller.PropertyChangeConstants;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

import dataTypes.Sudoku;
import dataTypes.SudokuBitSetImpl;

/**
 * 
 * @author Felix Schliephacke
 * 
 * @param <T>
 */
public final class BitSetSolver implements Runnable {

	@SuppressWarnings("unchecked")
	private static List allSolutions;
	@SuppressWarnings("unused")
	private static final float HASHMAPLOADFACTOR = 1f;
	@SuppressWarnings("unused")
	private static int INITIALHASHMAPCAPACITY;
	private static boolean interrupted = false;
	private static int maxSolutions = Integer.MAX_VALUE;
	private static int maxThreads = Runtime.getRuntime().availableProcessors();

	private static Long solutionCount = new Long(0);

	private static Integer threadCount = 0;

	public static BitSetSolver getInstance(Sudoku<Integer> sudoku) {
		return new BitSetSolver(sudoku);
	}

	// private static DatabaseConnection db = DatabaseConnection.getInstance();

	public static BitSetSolver getInstance(SudokuBitSetImpl sudoku) {
		return new BitSetSolver(sudoku);
	}

	public static int getMaxSolutions() {
		return maxSolutions;
	}

	public static int getMaxThreads() {
		return maxThreads;
	}

	public static Long getSolutionCount() {
		return solutionCount;
	}

	public static int getThreadCount() {
		return threadCount;
	}

	public static void setMaxSolutions(int value) {
		if (value < 1) {
			throw new IllegalArgumentException(
					"Number of maximum solutions must be greater than 0.");
		}
		maxSolutions = value;
	}

	public static synchronized void setMaxThreads(int value) {
		maxThreads = value;
	}

	private BitSetSolver recursiveSolver1;
	private BitSetSolver recursiveSolver2;
	private SudokuBitSetImpl sudoku;
	private PropertyChangeSupport support;
	private Thread thread;

	private BitSetSolver(Sudoku<Integer> sudoku) {
		this.sudoku = SudokuBitSetImpl.getInstance(sudoku);
		support = new PropertyChangeSupport(this);
		INITIALHASHMAPCAPACITY = this.sudoku.getSize();
	}

	private BitSetSolver(SudokuBitSetImpl sudoku) {
		this.sudoku = sudoku.clone();
		support = new PropertyChangeSupport(this);
		INITIALHASHMAPCAPACITY = this.sudoku.getSize();
	}

	public void addPropertyChangeListener(PropertyChangeListener listener) {
		if (this.support != null) {
			support.addPropertyChangeListener(listener);
		}
	}

	@SuppressWarnings("unchecked")
	public List<Sudoku> getSolutions() {
		List<Sudoku> temp = new LinkedList<Sudoku>();
		temp.addAll(allSolutions);
		return temp;
	}

	public SudokuBitSetImpl getSudoku() {
		return this.sudoku.clone();
	}

	@SuppressWarnings("unchecked")
	public void init() {
		interrupted = false;
		solutionCount = new Long(0);
		if (allSolutions != null) {
			for (Object temp : allSolutions) {
				((Sudoku) temp).finalize();
			}
		}
		allSolutions = null;
		allSolutions = new Vector<Sudoku>(100000, 100000);
		this.solveThread();
	}

	public void interrupt() {
		interrupted = true;
		if (this.recursiveSolver1 != null) {
			this.recursiveSolver1.interrupt();
		}
		if (this.recursiveSolver2 != null) {
			this.recursiveSolver2.interrupt();
		}
		if (this.support != null) {
			for (PropertyChangeListener listener : this.support
					.getPropertyChangeListeners()) {
				this.removePropertyChangeListener(listener);
			}
		}
		if (this.thread != null) {
			this.thread.interrupt();
		}
		this.thread = null;
	}

	public void join() throws InterruptedException {
		if (this.thread != null) {
			this.thread.join();
		}
	}

	public void removePropertyChangeListener(PropertyChangeListener listener) {
		if (this.support != null) {
			support.removePropertyChangeListener(listener);
		}
	}

	@Override
	public void run() {
		this.solve();
		synchronized (threadCount) {
			threadCount--;
		}
		this.finalize();
	}

	private Coordinates findWeakSpot() {
		int elements = Integer.MAX_VALUE;
		Coordinates temp = new Coordinates(Integer.MIN_VALUE, Integer.MIN_VALUE);
		for (int i = 0; i < this.sudoku.getSize(); i++) {
			for (int j = 0; j < this.sudoku.getSize(); j++) {
				if (this.sudoku.getBitSetField(i, j).cardinality() > 1
						&& this.sudoku.getBitSetField(i, j).cardinality() < elements) {
					elements = this.sudoku.getBitSetField(i, j).cardinality();
					temp = new Coordinates(i, j);
					if (elements == 2) {
						return temp;
					}
				}
			}
		}
		return temp;
	}

	private boolean isolateLonelyOccurences() {
		return false;
		// boolean changed = this.isolateLonelyOccurencesInRows();
		// if (!changed) {
		// changed = this.isolateLonelyOccurencesInColumns();
		// }
		// if (!changed) {
		// changed = this.isolateLonelyOccurencesInSquare();
		// }
		// return changed;
	}

	// private boolean isolateLonelyOccurencesInColumns() {
	// Field<T>[] content;
	// boolean changed = false;
	// HashMap<T, Integer> counter = new HashMap<T, Integer>(
	// INITIALHASHMAPCAPACITY, HASHMAPLOADFACTOR);
	//
	// for (int i = 0; i < this.sudoku.getSize(); i++) {
	// content = this.sudoku.getColumn(i);
	// counter.clear();
	// for (Field<T> field : content) {
	// if (field.size() > 1) {
	// for (T t : field) {
	// if (counter.containsKey(t)) {
	// counter.put(t, counter.get(t) + 1);
	// } else {
	// counter.put(t, 1);
	// }
	// }
	// } else if (field.size() == 1) {
	// counter.put(field.get(0), Integer.MIN_VALUE);
	// }
	// }
	// for (T key : counter.keySet()) {
	// if (counter.get(key) == 1) {
	// for (int j = 0; j < this.sudoku.getSize(); j++) {
	// if (content[j].contains(key)) {
	// this.sudoku.setField(j, i, key);
	// changed = true;
	// break;
	// }
	// }
	// }
	// }
	// }
	// return changed;
	// }
	//
	// private boolean isolateLonelyOccurencesInRows() {
	// Field<T>[] content;
	// boolean changed = false;
	// HashMap<T, Integer> counter = new HashMap<T, Integer>(
	// INITIALHASHMAPCAPACITY, HASHMAPLOADFACTOR);
	// for (int i = 0; i < this.sudoku.getSize(); i++) {
	// content = this.sudoku.getRow(i);
	// counter.clear();
	// for (Field<T> field : content) {
	// if (field.size() > 1) {
	// for (T t : field) {
	// if (counter.containsKey(t)) {
	// counter.put(t, counter.get(t) + 1);
	// } else {
	// counter.put(t, 1);
	// }
	// }
	// } else if (field.size() == 1) {
	// counter.put(field.get(0), Integer.MIN_VALUE);
	// }
	// }
	// for (T key : counter.keySet()) {
	// if (counter.get(key) == 1) {
	// for (int j = 0; j < this.sudoku.getSize(); j++) {
	// if (content[j].contains(key)) {
	// this.sudoku.setField(i, j, key);
	// changed = true;
	// break;
	// }
	// }
	// }
	// }
	// }
	// return changed;
	// }
	//
	// private boolean isolateLonelyOccurencesInSquare() {
	// Field<T>[] content;
	// boolean changed = false;
	// HashMap<T, Integer> counter = new HashMap<T, Integer>(
	// INITIALHASHMAPCAPACITY, HASHMAPLOADFACTOR);
	//
	// for (int i = 0; i < this.sudoku.getSize(); i++) {
	// counter.clear();
	// content = this.sudoku.getSquare(i);
	// for (Field<T> field : content) {
	// if (field.size() > 1) {
	// for (T t : field) {
	// if (counter.containsKey(t)) {
	// counter.put(t, counter.get(t) + 1);
	// } else {
	// counter.put(t, 1);
	// }
	// }
	// } else if (field.size() == 1) {
	// counter.put(field.get(0), Integer.MIN_VALUE);
	// }
	// }
	// int[] rowIndices = Help.calculateRowIndices(i, this.sudoku
	// .getSquareSize());
	// int[] columnIndices = Help.calculateColumnIndices(i, this.sudoku
	// .getSquareSize());
	// for (T key : counter.keySet()) {
	// if (counter.get(key) == 1) {
	// for (int j = 0; j < this.sudoku.getSquareSize(); j++) {
	// for (int k = 0; k < this.sudoku.getSquareSize(); k++) {
	// if (this.sudoku.getField(rowIndices[j],
	// columnIndices[k]).contains(key)) {
	// this.sudoku.setField(rowIndices[j],
	// columnIndices[k], key);
	// changed = true;
	// break;
	// }
	// }
	// }
	// }
	// }
	// }
	// return changed;
	// }

	private void recursion() {
		Coordinates weakSpot = this.findWeakSpot();
		int removedElement = this.sudoku.getField(weakSpot.getRow(), weakSpot
				.getColumn())[0];
		SudokuBitSetImpl recursiveSudoku1 = this.sudoku.clone();
		SudokuBitSetImpl recursiveSudoku2 = this.sudoku.clone();
		recursiveSudoku1.setField(weakSpot.getRow(), weakSpot.getColumn(),
				removedElement);
		recursiveSudoku2.removeFromField(weakSpot.getRow(), weakSpot
				.getColumn(), removedElement);
		recursiveSolver1 = BitSetSolver.getInstance(recursiveSudoku1);
		recursiveSolver2 = BitSetSolver.getInstance(recursiveSudoku2);
		for (PropertyChangeListener listener : this.support
				.getPropertyChangeListeners()) {
			recursiveSolver1.addPropertyChangeListener(listener);
			recursiveSolver2.addPropertyChangeListener(listener);
		}
		recursiveSolver1.start();
		recursiveSolver2.start();
		try {
			recursiveSolver1.join();
			recursiveSolver2.join();
		} catch (InterruptedException e) {
		}
	}

	private boolean removeSingleOccurences() {
		boolean changed = false;
		for (int i = 0; i < this.sudoku.getSize(); i++) {
			for (int j = 0; j < this.sudoku.getSize(); j++) {
				if (this.sudoku.getBitSetField(i, j).cardinality() == 1) {
					if (this.sudoku.removeFromContext(i, j, this.sudoku
							.getField(i, j)[0])) {
						changed = true;
					}
				}
			}
		}
		return changed;
	}

	@SuppressWarnings("unchecked")
	private void solve() {
		if (!interrupted) {
			boolean changed = true;
			while (changed && this.sudoku.isSolvable()
					&& !this.sudoku.isSolved()) {
				changed = false;
				while (this.removeSingleOccurences()) {
					changed = true;
				}
				if (this.isolateLonelyOccurences()) {
					changed = true;
				}
			}
			if (this.sudoku.isSolvable() && !this.sudoku.isSolved()) {
				this.recursion();
			}
			if (this.sudoku.isSolved() && !interrupted) {
				allSolutions.add(this.sudoku.convert());
				// db.insertSudoku(this.sudoku);
				synchronized (solutionCount) {
					support.firePropertyChange(
							PropertyChangeConstants.SOLUTIONS.toString(),
							solutionCount++, solutionCount);
					if (solutionCount >= maxSolutions) {
						this.interrupt();
					}
				}
			}
		}
		if (this.support != null) {
			for (PropertyChangeListener listener : this.support
					.getPropertyChangeListeners()) {
				this.removePropertyChangeListener(listener);
			}
		}
		this.finalize();
	}

	private void solveThread() {
		this.thread = new Thread(this);
		this.thread.start();
	}

	private void start() {
		boolean startAsThread = false;
		synchronized (threadCount) {
			if (threadCount < maxThreads) {
				threadCount++;
				startAsThread = true;
			}
		}
		if (startAsThread) {
			this.solveThread();
		} else {
			this.solve();
		}
	}

	@Override
	protected void finalize() {
		this.recursiveSolver1 = null;
		this.recursiveSolver2 = null;
		if (this.sudoku != null) {
			this.sudoku.finalize();
		}
		this.sudoku = null;
		this.support = null;
		this.thread = null;
	}
}